package org.argus.common.web.core.snowflake;

import lombok.extern.slf4j.Slf4j;
import org.argus.common.core.exception.ServiceException;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 雪花算法ID生成器
 * 借鉴百度UID雪花算法思路，依赖Redisson的原子递增ID生成WorkerId
 * @Author 刘文/Cliff Liu
 * @Date 2024/5/19
 */
@Slf4j
public class SnowflakeIdGenerator {

    private final long epochSeconds;

    private final WorkerIdAssigner workerIdAssigner;

    private final BitsAllocator bitsAllocator;

    private AtomicReference<Long> lastSecond = new AtomicReference<>(0L);

    private AtomicLong sequence = new AtomicLong(0L);

    private final long workerId;

    public SnowflakeIdGenerator(long epochSeconds, WorkerIdAssigner workerIdAssigner, BitsAllocator bitsAllocator) {
        this.epochSeconds = epochSeconds;
        this.workerIdAssigner = workerIdAssigner;
        this.bitsAllocator = bitsAllocator;
        this.workerId = workerIdAssigner.assignWorkerId();
    }

    public long nextId() {
        long currentSecond = getCurrentSecond();

        // Clock moved backwards, refuse to generate uid
        if (currentSecond < lastSecond.get()) {
            long refusedSeconds = lastSecond.get() - currentSecond;
            String message = "Clock moved backwards. Refusing for " + refusedSeconds + " seconds";
            log.error(message);
            throw new ServiceException(message);
        }

        // At the same second, increase sequence
        if (currentSecond <= lastSecond.get()) {
            long curSequence = sequence.incrementAndGet();
            // Exceed the max sequence, we wait the next second to generate uid
            if (curSequence >= bitsAllocator.getMaxSequence()) {
                currentSecond = getNextSecond(lastSecond.get());
            }

            // At the different second, sequence restart from zero
        } else {
            lastSecond.set(currentSecond);
            sequence.set(0L);
        }

        // Allocate bits for UID
        return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence.get());

    }

    /**
     * Get next second
     */
    private long getNextSecond(long lastTimestamp) {
        long timestamp = getCurrentSecond();
        while (timestamp <= lastTimestamp) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                log.error("Interrupted while waiting next second");
            }
            timestamp = getCurrentSecond();
        }

        return timestamp;
    }

    /**
     * Get current second
     */
    private long getCurrentSecond() {
        long currentSecond = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
        if (currentSecond - epochSeconds > bitsAllocator.getMaxDeltaSeconds()) {
            String message = "Timestamp bits is exhausted. Refusing UID generate. Now: " + currentSecond;
            log.error(message);
            throw new ServiceException(message);
        }

        return currentSecond;
    }
}
